# -*- coding: utf-8 -*-
# @Author  : pony.guan
# @Time    : 10:09
# Description: 调用ess_p标准品接口实现入库、出库、移库、循环移库 以及各种校验工作

import os
import random
import uuid
import json
import time
from itertools import zip_longest

import requests
import zipfile
from tenacity import retry, stop_after_delay, stop_after_attempt, wait_fixed, retry_if_result

from mylog import log


class Order:

    def __init__(self, ip: str, port: int = 19000, debug: bool = True):
        """
        :param ip: ip或域名
        :param port:
        :param debug:
        """

        self.debug = debug

        if ip.replace('.', '').isdigit():  # ip
            pre_ = f'{ip}:{port}'
        else:  # 域名
            pre_ = ip

        self.ess_p_url = {
            # 地图导出
            'map_export': f'http://{pre_}/map/export',
            # 库位列表
            'location_lst': f'http://{pre_}/ess-api/location/query',
            # 入库任务
            'enter_location': f'http://{pre_}/wms/createWmsInboundTask',
            # 出库任务
            'out_location': f'http://{pre_}/wms/createWmsOutboundTask',
            # 移库任务
            'move_location': f'http://{pre_}/wms/createWmsRelocationTask',

            # 容器创建
            'add_container': f'http://{pre_}/container/add',
            # 容器进场
            'container_mv_location': f'http://{pre_}/container/moveIn',
            # 任务状态查询
            'task_query': f'http://{pre_}/wms/taskQuery',

            # 容器详情(容器当前位置等)
            'container_query': f'http://{pre_}/container/query',
            # 库位详情查询(库位上有无容器)
            'location_query': f'http://{pre_}/wms/locationQuery',
            # 容器离场
            'move_out': f'http://{pre_}/container/moveOut'
        }

        self.headers = {
            "Content-Type": "application/json"
        }

        self.task_type = {
            'in': '入库',
            'out': '出库',
            're': '移库'
        }

        self.map_data = self.get_map_data()

        self.empty_location_code_lst = []

        self.error = {}

        self.success_move_out_lst = []

        # 取箱口的库位类型
        self.move_in_from_type = ['LT_CONVEYOR_OUTPUT', 'LT_CACHE_SHELF_STORAGE', 'LT_CONVEYOR_INOUT']

        # 放箱口的库位类型
        self.move_out_to_type = ['LT_CONVEYOR_INPUT', 'LT_CACHE_SHELF_STORAGE', 'LT_CONVEYOR_INOUT']

    def get_map_data(self):
        """
        加载地图数据
        :return:
        """
        r = requests.get(self.ess_p_url['map_export'], headers=self.headers)
        map_zip_file = 'map.zip'
        if r.status_code == 200:
            with open(map_zip_file, 'wb') as f:
                f.write(r.content)
        zf = zipfile.ZipFile(map_zip_file, "r")

        content = zf.read(zf.namelist()[0])

        with open('bb.json', 'wb') as f:
            f.write(content)
        zf.close()
        return json.loads(content)

    def add_container(self, container_code_lst: list):
        """
        创建容器
        :param container_code_lst: 容器列表
        :return:
        """
        data = {
            'containerAdds': [{'containerCode': container, 'containerTypeCode': "CT_KUBOT_STANDARD"} for container in
                              container_code_lst]}

        r = requests.post(url=self.ess_p_url['add_container'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)

        if r.json()['code'] != 0:
            log.error('创建容器失败: \n数据:\n' + json.dumps(data, indent=4) + '\n响应:\n' + json.dumps(r.json(), indent=4))
            return
        log.info('创建容器成功: \n' + json.dumps(data, indent=4) + '\n' + json.dumps(r.json(), indent=4))
        add_container_lst = list(r.json()['data'].keys())
        return add_container_lst

    def container_mv_location(self, cc_pc_lst: list) -> dict:
        """
        @desc 容器进场
        :param cc_pc_lst: [(容器code, 取箱口code)]
        :return:
        """
        data = {"containerMoveIns": [{'containerCode': i, 'positionCode': j} for i, j in cc_pc_lst]}
        r = requests.post(url=self.ess_p_url['container_mv_location'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)

        if r.json()['code'] != 0:
            log.error('move in 失败: \n数据：\n' + json.dumps(data, indent=4) + '\n响应：\n' + json.dumps(r.json(), indent=4))
            return {
                'code': 1001,
                'error': '容器入场失败'
            }
        log.info('move in 成功: \n' + json.dumps(data, indent=4) + '\n' + json.dumps(r.json(), indent=4))
        moved_in_container = list(r.json()['data'].keys())
        return {
            'code': '1000',
            'data': f"容器：{','.join(moved_in_container)}, 入场成功"
        }

    def add_mv_container(self, new_cc_fc_lst: list):
        """
        新容器(不存在)的容器 进行 创建 和 move in
        :return:
        """
        new_cc_lst = [i[0] for i in new_cc_fc_lst]
        added_cc_lst = self.add_container(new_cc_lst)
        if added_cc_lst:
            self.container_mv_location(new_cc_fc_lst)

    def get_cft(self, cft_lst, cc_fc_dic_=None):
        """
        @desc ① 测试环境，不存在的容器需要创建和move in 库位  ② 没有to_location_code时，需要分配
        :param cft_lst:
        :return:
        """
        if cc_fc_dic_ is None:
            cc_fc_dic_ = {}
        cft_lst = [cft for cft in cft_lst if cft['container_code']]
        location_lst = self.map_data['location']

        # 没有容器的库位
        if not self.empty_location_code_lst:
            empty_location_code_lst = [location['code'] for location in location_lst if
                                       not location.get('containerCode', '') and location[
                                           'locationTypeCode'] == 'LT_SHELF_STORAGE']
            self.empty_location_code_lst = empty_location_code_lst

        # 不存在于库位中的容器需要创建, move in
        new_cc_fc_lst = []
        for cft in cft_lst:
            container_code = cft['container_code']
            from_location_code = cft['from_location_code']
            fc = cc_fc_dic_.get(container_code, None)
            to_location_code = cft['to_location_code']

            if from_location_code is None:  # 不存在于库位中的容器  (正式环境不会走这里)
                if fc:  # 测试环境下发移库任务，且为不存在的容器指定了源库位，才会走到这里
                    from_location_code = fc
                    self.empty_location_code_lst.remove(from_location_code)
                else:
                    from_location_code = self.empty_location_code_lst.pop(0)
                cft['from_location_code'] = from_location_code
                new_cc_fc_lst.append((container_code, from_location_code))

            self.empty_location_code_lst.append(from_location_code)

            if to_location_code is None:
                # 随机获取目标库位
                to_location_code = random.choice(self.empty_location_code_lst)
                self.empty_location_code_lst.remove(to_location_code)
                cft['to_location_code'] = to_location_code
                # self.empty_location_code_lst.append(to_location_code)

        if new_cc_fc_lst:  # (正式环境不会走这里)
            self.add_mv_container(new_cc_fc_lst)
        return cft_lst

    def is_valid(self, container_code_lst: list) -> dict:
        """
        @desc ①正式环境 容器一定要事先由员工手动创建、move in、搬到库位上
              ②测试环境，允许容器不存在于库位上
        :return: 容器和容器所在的库位
        """
        # 所有存在于库位中的料箱
        location_lst = self.map_data['location']

        cc_fc_dic = {}
        for location in location_lst:
            cc = location.get('containerCode', '')
            if cc in container_code_lst:
                cc_fc_dic[cc] = location['code']

        diff = set(container_code_lst) - set(cc_fc_dic.keys())
        if not self.debug:
            if diff:
                error_msg = f'库位中没有以下容器：{str(diff)}'
                log.error(error_msg)
                self.error = {
                    'code': 1001,
                    'error': error_msg
                }
            else:
                return cc_fc_dic
        else:
            list(map(lambda x: cc_fc_dic.setdefault(x, None), container_code_lst))
            return cc_fc_dic

    def move_location_task(self,
                           container_code_lst: list,
                           from_location_code_lst: list,
                           to_location_code_lst: list,
                           cycle=False) -> dict:
        """
        @desc 下发移库任务
        :param container_code_lst: 容器code列表
        :param from_location_code_lst: 源库位列表
        :param to_location_code_lst: 目标库位列表
        :param cycle: 标记是否为循环移库调用
        :return:
        """
        cc_fc_dic_ = None
        if not cycle:
            log.info('开始移库')
            dic_ = {}
            for cc, fc, tc in zip(container_code_lst, from_location_code_lst, to_location_code_lst):
                dic_[cc] = {'from_location_code': fc, 'to_location_code': tc}
            cc_fc_dic = self.is_valid(container_code_lst)
            if self.error:
                return self.error
            container_code_lst = list(cc_fc_dic.keys())
            # from_location_code_lst = list(cc_fc_dic.values()) if not from_location_code_lst else from_location_code_lst
            from_location_code_lst = list(cc_fc_dic.values())
            to_location_code_lst = [dic_[cc]['to_location_code'] for cc in container_code_lst]

            if self.debug:  # 测试环境
                # 为不存在的容器指定了from_location的, 需要去判断from_location上是否有容器，有则返回错误，无则需要把新容器move in到这里
                cc_lst_ = [cc for cc, fc in cc_fc_dic.items() if fc is None]  # 不存在的容器
                # cc_fc_dic_: key---不存在的容器， value：给该容器指定了的库位
                cc_fc_dic_ = {cc: dic_[cc]['from_location_code'] for cc in cc_lst_ if dic_[cc]['from_location_code']}
                fc_cc_dic_ = {value: key for key, value in cc_fc_dic_.items()}

                diff = set(fc_cc_dic_.keys()) - set([location['code'] for location in self.map_data['location']])
                if diff:
                    error_msg = f'不存在以下源库位{str(diff)}'
                    log.info(error_msg)
                    return {
                        'code': 1001,
                        'error': error_msg
                    }

                for location in self.map_data['location']:
                    location_code = location['code']
                    if location_code in fc_cc_dic_:
                        if location.get('containerCode'):
                            error_msg = f'新容器{fc_cc_dic_[location_code]}不能move in到库位{location_code},该库位上已经存在容器了'
                            log.info(error_msg)
                            return {
                                'code': 1001,
                                'error': error_msg
                            }

            # 校验to_location_code

            # 已经指定的目标库位是否存在， 是否上面没有料箱
            # 没有指定目标库位的容器，则分配空库位

            location_lst = self.map_data['location']
            # 指定的目标库位
            mark_tc = []
            for cc in dic_:
                tc = dic_[cc]['to_location_code']
                if tc:
                    mark_tc.append(tc)

            diff = set(mark_tc) - set([location['code'] for location in location_lst])
            if diff:
                error_msg = f'不存在以下目标库位{str(diff)}'
                log.info(error_msg)
                return {
                    'code': 1001,
                    'error': error_msg
                }

            for location in location_lst:
                location_code = location['code']
                if location_code in mark_tc:
                    if location.get('containerCode'):
                        error_msg = f'目标库位{location_code}已经存在容器了'
                        log.info(error_msg)
                        return {
                            'code': 1001,
                            'error': error_msg
                        }

        cft_lst = [{'container_code': i,
                    'from_location_code': j,
                    'to_location_code': k
                    }
                   for i, j, k in
                   zip_longest(container_code_lst,
                               from_location_code_lst or [],
                               to_location_code_lst or [],
                               fillvalue=None)]
        cft_lst = self.get_cft(cft_lst, cc_fc_dic_)

        tasks = []
        for cft in cft_lst:
            container_code = cft['container_code']
            from_location_code = cft['from_location_code']
            to_location_code = cft['to_location_code']
            task_code = 'Re_task' + str(uuid.uuid4())

            tasks.append({
                'taskTemplateCode': 'TOTE_RELOCATION',  # 固定
                'taskCode': task_code,  # 唯一
                'taskPriority': 0,  # 固定
                'taskDescribe': {
                    'containerCode': container_code,
                    'fromLocationCode': from_location_code,
                    'toLocationCode': to_location_code
                }
            })
        task_group_code = 'g_' + str(uuid.uuid4())
        data = {
            "taskGroupCode": task_group_code,  # 唯一
            "groupPriority": 0,  # 0
            "tasks": tasks
        }
        r = requests.post(url=self.ess_p_url['move_location'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        if r.json()['code'] != 0:
            log.error('移库任务下发失败: \n数据:\n' + json.dumps(data, indent=4) + '\n 响应:\n' + json.dumps(r.json(), indent=4))
            return {
                'code': 1001,
                'error': '移库任务下发失败'
            }

        log.info('移库任务下发成功: \n数据:\n' + json.dumps(data, indent=4) + '\n响应:\n' + json.dumps(r.json(), indent=4))

        if cycle:
            task_lst = [{'task_code': task['taskCode'],
                         'cc': task['taskDescribe']['containerCode'],
                         'fc': task['taskDescribe']['fromLocationCode'],
                         'tc': task['taskDescribe']['toLocationCode'],
                         } for task in tasks]
            return {
                'code': 1000,
                'data': task_lst
            }
        return {
            'code': 1000,
            'data': '移库任务下发成功'
        }

    @retry(stop=(stop_after_delay(15 * 60) | stop_after_attempt(1000000)), wait=wait_fixed(25),
           retry=retry_if_result(lambda value: value is False), reraise=True)
    def check_task_status(self, task_code_lst: list, count: int, task_type: str = 're'):
        """
        检查业务任务状态
        """
        data = {
            "taskCodes": task_code_lst
        }
        r = requests.post(url=self.ess_p_url['task_query'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        if r.json()['code'] != 0:
            # log.error('查询移库任务失败: \n数据:\n' + json.dumps(data, indent=4) + '\n 响应:\n' + json.dumps(r.json(), indent=4))
            log.error(f'查询第{count}轮{self.task_type[task_type]}任务失败')
            return {
                'code': 1001,
                'error': f'查询第{count}轮{self.task_type[task_type]}任务失败'
            }

        # log.info('查询移库任务成功: \n数据:\n' + json.dumps(data, indent=4) + '\n响应:\n' + json.dumps(r.json(), indent=4))
        log.info(f'查询第{count}轮{self.task_type[task_type]}任务成功')
        tasks = r.json()['data']['tasks']

        if all([task['taskStatus'] == 'FINISHED' for task in tasks]):
            log.info(f'第{count}轮{self.task_type[task_type]}任务全部执行成功')
            return True
        else:
            log.info(f'第{count}轮{self.task_type[task_type]}任务没有全部执行成功, 稍后重新查询')
            return False

    def cycle_move_location_task(self, container_code_lst: list, end_dt: float = float('inf')) -> dict:
        """
        @desc 循环下发移库任务
        :param container_code_lst: 容器code列表， 不可为空列表
        :param end_dt: 循环结束时间戳
        :return:
        """
        cc_fc_dic = self.is_valid(container_code_lst)
        if self.error:
            return self.error
        container_code_lst, from_location_code_lst = list(cc_fc_dic.keys()), list(cc_fc_dic.values())

        log.info('开始循环移库')
        with open('cycle_flag.txt', 'w') as f:
            pass
        count = 0
        while os.path.exists('cycle_flag.txt') and time.time() < end_dt:
            count += 1
            ret = self.move_location_task(container_code_lst, from_location_code_lst, [], cycle=True)

            if ret['code'] == 1001:
                return ret
            # 查询每一轮的库任务的结果状态
            task_lst = ret['data']
            task_code_lst = [task['task_code'] for task in task_lst]
            try:
                self.check_task_status(task_code_lst, count)
                from_location_code_lst = [task['tc'] for task in task_lst]
                log.info(f'第{count}轮移库任务执行成功（finished）: \n数据:\n' + json.dumps(task_lst, indent=4))
            except Exception:
                log.info(f'第{count}轮移库任务执行超时:15分钟内没有全部执行完成（not finished）: \n数据:\n' + json.dumps(task_lst, indent=4))
                return {
                    'code': 1001,
                    'error': f'第{count}轮移库任务执行超时:15分钟内没有全部执行完成'
                }

        msg = '循环移库结束'
        log.info(msg)
        return {
            'code': 1000,
            'data': msg
        }

    @staticmethod
    def end_cycle_move_location_task():
        """
        手动方式结束循环移库任务
        :return:
        """
        os.remove('cycle_flag.txt')

    def enter_location_task(self,
                            container_code_lst: list,
                            from_location_code_lst: list,
                            to_location_code_lst: list,
                            ) -> dict:
        """
        @desc下发入库任务: ①创建容器--->②下发入库任务--->③move in
        注意：
            1. move in只能从同一个取箱口入库
            2. 批量下发任务后
                            测试环境： 等10s后去执行move in
                            正式环境： 无需在这里执行move in
            3. move in:
                     测试环境: 一次只能 move in 一个料箱， 且上一个料箱不在取箱口了，才对下一个料箱执行move in
                     正式环境: 批量下发入库任务后，人为去搬箱子， 认为搬一个，告知一个(containerCode， positionCode),调用move in
        :param container_code_lst: 容器code列表
        :param from_location_code_lst: 取箱口列表
        :param to_location_code_lst: 目标库位列表
        :return:
        """
        log.info('开始入库')
        dic_ = {}
        for cc, fc, tc in zip(container_code_lst, from_location_code_lst, to_location_code_lst):
            dic_[cc] = {'from_location_code': fc, 'to_location_code': tc}

        # 校验 container_code_lst 必须全部不在库位中
        all_exist_cc_lst = [location['containerCode'] for location in self.map_data['location'] if
                            location.get('containerCode', '')]

        intersection = set(container_code_lst) & set(all_exist_cc_lst)
        if intersection:
            return {
                'code': 1001,
                'error': f'以下容器已经位于库位中 {str(intersection)}, 不能进行入库操作'
            }

        log.info('开始入库')
        # ①创建容器： 判断容器是否存在, 不存在，则需要创建
        all_container_lst = [i['code'] for i in self.map_data['container']]
        diff_lst = list(set(container_code_lst) - set(all_container_lst))
        if diff_lst:
            self.add_container(diff_lst)

        # 校验取箱口
        empty_position_code_lst = [location['code'] for location in self.map_data['location'] if
                                   (location['locationTypeCode'] in self.move_in_from_type and not location.get(
                                       'containerCode', ''))]

        if not empty_position_code_lst:
            return {
                'code': 1001,
                'error': f'所有取箱口上都已经存在容器了'
            }

        diff = set([position_code for position_code in from_location_code_lst if position_code]) - set(
            empty_position_code_lst)
        if diff:
            return {
                'code': 1001,
                'error': f'以下取箱口{str(diff)}已经在容器 或者 不存在'
            }

        # ②下发入库任务
        # 给没有指定目标库位的容器 分配空库位
        location_lst = self.map_data['location']
        empty_location_code_lst = [location['code'] for location in location_lst if
                                   not location.get('containerCode', '') and location[
                                       'locationTypeCode'] == 'LT_SHELF_STORAGE']

        if not empty_location_code_lst:
            return {
                'code': 1001,
                'error': f'所有目标库位上都已经存在容器了'
            }

        diff = set([location_code for location_code in to_location_code_lst if location_code]) - set(
            empty_location_code_lst)
        if diff:
            return {
                'code': 1001,
                'error': f'以下目标库位{str(diff)}已经在容器 或者 不存在'
            }

        tasks = []
        for container_code in dic_:
            location_code = dic_[container_code]['to_location_code']
            if not location_code:
                location_code = empty_location_code_lst.pop()
            task_code = 'In_task' + str(uuid.uuid4())
            tasks.append({
                "taskTemplateCode": "TOTE_INBOUND",
                "taskCode": task_code,
                "taskPriority": 0,
                "taskDescribe": {
                    "containerCode": container_code,
                    "locationCode": location_code
                }
            })

        task_group_code = 'g_' + str(uuid.uuid4())
        data = {
            'taskGroupCode': task_group_code,
            'groupPriority': 0,
            'tasks': tasks
        }

        r = requests.post(url=self.ess_p_url['enter_location'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        if r.json()['code'] != 0:
            log.error('入库任务下发失败: \n数据:\n' + json.dumps(data, indent=4) + '\n 响应:\n' + json.dumps(r.json(), indent=4))
            return {
                'code': 1001,
                'error': '入库任务下发失败'
            }

        log.info('入库任务下发成功: \n数据:\n' + json.dumps(data, indent=4) + '\n响应:\n' + json.dumps(r.json(), indent=4))

        if not self.debug:
            return {
                'code': 1000,
                'data': '入库任务下发成功, 请人工搬箱后，再执行容器进场操作'
            }

        time.sleep(10)

        # 没有指定取箱口的的容器，全都绑定到取箱口position_code_flag上
        position_code_flag = empty_position_code_lst[0]

        for container_code in dic_:
            position_code = dic_[container_code]['from_location_code']
            if not position_code:
                position_code = position_code_flag

            cc_pc_lst = [(container_code, position_code)]
            self.container_mv_location(cc_pc_lst)

            # 容器move in 后, 确保取箱口没有该容器后，才继续下一个容器的move in
            try:
                # time.sleep(10)
                self.location_is_empty(position_code)
                log.info(f'入库成功： {container_code}--->{position_code_flag}')
            except Exception:
                return {
                    'code': 1001,
                    'error': f'取箱口{position_code_flag}10分钟内一直有容器'
                }

        log.info('所有容器入库完成')
        return {
            'code': 1000,
            'data': '入库完成'
        }

    @retry(stop=(stop_after_delay(10 * 60) | stop_after_attempt(1000000)), wait=wait_fixed(10),
           retry=retry_if_result(lambda value: value is False), reraise=True)
    def location_is_empty(self, location_code):
        """
        检测是否为空库位
        :param location_code:
        :return:
        """
        data = {"locationCodes": [location_code]}
        r = requests.post(url=self.ess_p_url['location_query'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        cc_ = r.json()['data']['locations'][0].get('loadContainerCode', '')
        if not cc_:
            log.info(f'查询取箱口：{location_code} 为空')
            return True
        else:
            log.info(f'查询取箱口：{location_code} 不为空， 10后继续查询')
            return False

    @retry(stop=(stop_after_delay(10 * 60) | stop_after_attempt(1000000)), wait=wait_fixed(10),
           retry=retry_if_result(lambda value: value is False), reraise=True)
    def _test_move_out_io_loop(self, container_code_lst, position_code_lst):
        """
        @desc 测试环境出库IoLoop:  监听position_code_lst 列表中的库位是否有料箱到来
            遍历 position_code_lst， 一旦某一个position上有容器，就执行move out
        :param position_code_lst:
        :return:
        """
        data = {"locationCodes": position_code_lst}
        r = requests.post(url=self.ess_p_url['location_query'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)

        for location in r.json()['data']['locations']:
            location_code = location['locationCode']
            container_code = location.get('loadContainerCode', '')
            if container_code:
                log.info(f'监听到放箱口{location_code}上存在容器，将执行move out 操作')
                ret = self.move_out([(container_code, location_code)])
                if ret['code'] == 1000:
                    self.success_move_out_lst.append(container_code)

        diff = set(container_code_lst) - set(self.success_move_out_lst)
        if diff:
            return False
        else:
            return True

    def out_location_task(self, container_code_lst: list, to_station_code_lst: list) -> dict:
        """
        @desc 当前仅为工作站出库
                流程：
                    ①检测容器是否在库位上，没有需要创建和move in
                    ②分配工作站且确保存在一个工作站的出库口没有容器， 以下发出库任务  (正式环境下发任务后就结束，不在这里执行move out)
                    ③检查任务是否完成
                    ④容器离场:
                           测试环境：所有任务下发后，给所有容器分配的库位， 一直遍历这些库位， 一旦库位有容器，就执行move out
                                   并记录move out 的容器， move out 的容器集合 与 set(container_code_lst)相等时，
                                   全部容器离场成功
                           正式环境：所有任务下发后， 人为搬箱子，搬一个给一个positoncode, 以调用move out
        :param container_code_lst: 容器code列表，
        :param to_station_code_lst: 工作站code列表，
        :return:
        """
        log.info('开始出库')
        dic_ = dict(zip(container_code_lst, to_station_code_lst))

        # ① 如果容器不在库位上 测试环境需要创建和move in ,正式环境直接返回
        cc_fc_dic = self.is_valid(container_code_lst)
        if self.error:
            return self.error

        cc_lst = list(cc_fc_dic.keys())
        fc_lst = list(cc_fc_dic.values())

        cft_lst = [{'container_code': i,
                    'from_location_code': j,
                    'to_location_code': k
                    }
                   for i, j, k in zip_longest(cc_lst, fc_lst, [], fillvalue='pass')]

        self.get_cft(cft_lst)

        # ② 分配工作站且确保存在一个工作站的放箱口没有容器， 以下发出库任务
        lc_ts_dic = {}  # key: 放箱口， value： [放箱口所在工作站, 出库口是否有容器]

        for station in self.map_data['locationActor']:
            for key in station['boundLocation'].keys():
                if any([i in key for i in self.move_out_to_type]):
                    # if 'LT_CONVEYOR_INPUT' in key:
                    lc_ts_dic[key] = [station['code'], False]

        # 确保存在一个工作站的放箱口没有容器
        lc_ts_flag = False
        # 所有的存在空放箱口的工作站
        empty_to_station_code_lst = []
        for location in self.map_data['location']:
            if location['code'] in lc_ts_dic:
                cc = location.get('containerCode', False)
                lc_ts_dic[location['code']][-1] = cc
                if not cc:
                    empty_to_station_code_lst.append(lc_ts_dic[location['code']][0])
                    if not lc_ts_flag:
                        lc_ts_flag = [location['code'], lc_ts_dic[location['code']][0]]

        if not lc_ts_flag:
            return {
                'code': 1001,
                'error': '所有工作站都没有空的放箱口了'
            }

        diff = set([i for i in to_station_code_lst if i]) - set(empty_to_station_code_lst)

        if diff:
            return {
                'code': 1001,
                'error': f'以下工作站{str(diff)}均没有空的放箱口了,或者工作站不存在'
            }

        tasks = []

        flag_ = False
        for cc, tsc in dic_.items():
            if not tsc:
                flag_ = True
                tsc = lc_ts_flag[-1]
            task_code = 'Out_task_' + str(uuid.uuid4())
            tasks.append({
                "taskTemplateCode": "TOTE_CONVEYOR_STATION_OUTBOUND",
                "taskCode": task_code,
                "taskPriority": 0,
                "taskDescribe": {
                    "containerCode": cc,
                    "toStationCode": tsc
                }
            })

        task_group_code = 'g_' + str(uuid.uuid4())
        data = {
            'taskGroupCode': task_group_code,
            'groupPriority': 0,
            'tasks': tasks
        }

        r = requests.post(url=self.ess_p_url['out_location'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        if r.json()['code'] != 0:
            log.error('出库任务下发失败: \n数据:\n' + json.dumps(data, indent=4) + '\n 响应:\n' + json.dumps(r.json(), indent=4))
            return {
                'code': 1001,
                'error': '出库任务下发失败'
            }

        log.info('出库任务下发成功: \n数据:\n' + json.dumps(data, indent=4) + '\n响应:\n' + json.dumps(r.json(), indent=4))

        if not self.debug:
            return {
                'code': '1000',
                'data': '出库任务下发成功'
            }

        # # ③检查任务是否完成
        # task_code_lst = [task['taskCode'] for task in tasks]
        #
        # try:
        #     time.sleep(10)
        #     self.check_task_status(task_code_lst, 0, 'out')
        #     log.info(f'第0轮出库任务执行成功（finished）: \n数据:\n' + json.dumps(tasks, indent=4))
        # except Exception:
        #     log.info(f'出库任务执行超时:15分钟内没有全部执行完成（not finished）: \n数据:\n' + json.dumps(tasks, indent=4))
        #     return {
        #         'code': 1001,
        #         'error': f'出库任务执行超时:15分钟内没有全部执行完成'
        #     }

        # 需要监听的放箱口列表
        selected_station_code_lst = list(
            set([i for i in to_station_code_lst if i] + ([lc_ts_flag[-1]] if flag_ else [])))
        position_code_lst = []
        for position_code in lc_ts_dic:
            if lc_ts_dic[position_code][0] in selected_station_code_lst:
                position_code_lst.append(position_code)

        try:
            # ④容器逐个离场
            log.info(f'开始监听{len(position_code_lst)}个放箱口是否有容器：{str(position_code_lst)}')
            self._test_move_out_io_loop(cc_lst, position_code_lst)
            return {
                'code': 1000,
                'data': 'move out 成功'
            }
        except Exception:
            return {
                'code': 1001,
                'error': f"move out成功的容器：{str(self.success_move_out_lst)}"
                         f"move out失败的容器：{str(list(set(cc_lst) - set(self.success_move_out_lst)))}"
            }

    def move_out(self, cc_pc_lst: list) -> dict:
        """
        @desc 容器离场
        :param cc_pc_lst: [(容器code, 放箱口code)]
        :return:
        """
        cc, pc = cc_pc_lst[0]

        if cc and pc:
            container_move_outs = [{'containerCode': cc, 'positionCode': pc}]
        elif cc:
            container_move_outs = [{'containerCode': cc}]
        elif pc:
            container_move_outs = [{'positionCode': pc}]
        else:
            return {
                'code': 1001,
                'error': '容器, 放箱口 不能同时为空'
            }

        data = {
            "containerMoveOuts": container_move_outs
        }
        r = requests.post(url=self.ess_p_url['move_out'], data=json.dumps(data, ensure_ascii=False),
                          headers=self.headers)
        if r.json()['code'] != 0:
            log.error('容器离场失败: \n数据:\n' + json.dumps(data, indent=4) + '\n 响应:\n' + json.dumps(r.json(), indent=4))
            return {
                'code': 1001,
                'error': '容器离场失败'
            }
        log.info('离场成功')
        return {
            'code': 1000,
            'data': '离场成功'
        }


if __name__ == '__main__':
    ip = '172.20.3.230'


    def mv():
        o = Order(ip, debug=True)
        container_code_lst = ['y1', 'y2', 'y3', 'y4']
        o.move_location_task(container_code_lst, [], [])


    def cycle(debug):
        o = Order(ip, debug=debug)
        end_dt = time.time() + 60 * 3
        container_code_lst = ['y1', 'y8']
        ret = o.cycle_move_location_task(container_code_lst, end_dt=end_dt)
        print(ret)


    def out():
        o = Order(ip, debug=True)
        o.out_location_task(['w1', 'w2'])


    def enter_():
        o = Order(ip, debug=True)
        o.enter_location_task(['kkk1', 'kkk2', 'kkk3', 'kkk4'])


    #
    # enter_()
    # mv()
    # out()
    # cycle(debug=True)

    # 1、移除机器人
    # 2、actor/restartController           ----重启
    # 3、/mock/oneClickCreateMockKubot          ------启动mockkubot  （需要在body中填写机器人编号）
    # 4. http://172.20.8.56/robot-manage/robot-monitor 点击系统恢复

    # 接口调用说明

    # debug:
    #     True表示测试环境
    #     False表示正式环境
    ip = '172.20.3.230'
    Order(ip, debug=False)
    assert False
    # 移库：
    Order(ip, debug).move_location_task(container_code_lst, from_location_code_lst, to_location_code_lst)

    # 循环移库：
    Order(ip, debug).cycle_move_location_task(container_code_lst, end_dt)

    # 结束循环移库：
    Order(ip, debug).end_cycle_move_location_task()

    # 入库:
    Order(ip, debug).enter_location_task(container_code_lst, from_location_code_lst, to_location_code_lst)

    # 正式环境，还需逐个调用容器进场

    # 出库:
    Order(ip, debug).out_location_task(container_code_lst, to_station_code_lst)
    # 正式环境，还需调用 容器离场

    # 容器离场
    Order(ip, debug).move_out(cc_pc_lst)

    # 容器进场
    Order(ip, debug).container_mv_location(cc_pc_lst)
